﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace llAma
{
    public static class CacheStats
    {
        static long missCount = 0;
        static long lookupCount = 0;
        public static long Misses
        {
            get { return missCount; }
        }
        public static long Hits
        {
            get { return lookupCount - missCount; }
        }
        internal static void IncLookup()
        {
            Interlocked.Increment(ref lookupCount);
        }
        internal static void IncMiss()
        {
            Interlocked.Increment(ref missCount);
        }
    }
    public class ServerCache<TKey, T>
    {
        Dictionary<TKey, CacheEntry<T>> mCacheDict = new Dictionary<TKey, CacheEntry<T>>();
        public delegate CacheEntry<T> CacheMissDelegate(TKey key);
        private TimeSpan mDefaultTTL = new TimeSpan(0, 0, 30);
        public TimeSpan TimeToLive
        {
            get { return mDefaultTTL; }
            set { mDefaultTTL = value; }
        }
        public ServerCache(TimeSpan ttl)
        {
            mDefaultTTL = ttl;
        }
        public ServerCache()
        { }
        CacheMissDelegate OnCacheMiss = delegate(TKey key) { return default(CacheEntry<T>); };
        public CacheMissDelegate CacheMiss
        {
            get { return OnCacheMiss; }
            set { OnCacheMiss = value; }
        }
        public void DeleteCacheObject(TKey key)
        {
            lock (mCacheDict)
            {
                if (mCacheDict.ContainsKey(key)) mCacheDict.Remove(key);
            }
        }
        public void SetCacheObject(TKey key, CacheEntry<T> obj)
        {
            lock (mCacheDict)
            {
                mCacheDict[key] = obj;
            }
        }
        public void SetCacheObject(TKey key, T obj)
        {
            SetCacheObject(key, new CacheEntry<T>(obj, mDefaultTTL));
        }
        public T GetCacheObject(TKey key, CacheMissDelegate OnMiss)
        {
            //speed it up.
            CacheStats.IncLookup();
            if (mCacheDict.ContainsKey(key) && !mCacheDict[key].IsExpired)
                return mCacheDict[key].Value;
            lock (mCacheDict)
            {
                if (!mCacheDict.ContainsKey(key) || mCacheDict[key].IsExpired)
                {
                    mCacheDict[key] = OnMiss(key);
                    //what we do next, is kinda backwards.
                    CacheEntry<T> tmp = new CacheEntry<T>(mCacheDict[key].Value, mDefaultTTL);
                    tmp.ShouldExpireThis = mCacheDict[key].ShouldExpireThis;
                    CacheStats.IncMiss();
                }
                return mCacheDict[key].Value;
            }
        }
        public T this[TKey key]
        {
            get
            {
                return GetCacheObject(key);
            }
        }
        public T GetCacheObject(TKey key)
        {
            return GetCacheObject(key, OnCacheMiss);
        }
        public void WipeCache()
        {
            mCacheDict.Clear();
        }
    }
    public class CacheEntry<T>
    {
        DateTime mLastTouch;
        TimeSpan mLifeSpan;
        DateTime Expires
        {
            get
            {
                return mLastTouch + mLifeSpan;
            }
        }
        T mValue;
        Timer expireTimer;
        Predicate<T> shouldExpire = delegate(T obj)
        {
            return true;
        };
        public Predicate<T> ShouldExpireThis
        {
            set { shouldExpire = value; }
            internal get { return shouldExpire; }
        }
        public CacheEntry(T obj) : this(obj, new TimeSpan(0, 1, 0)) { }
        public CacheEntry(T obj, TimeSpan lifeSpan)
        {
            mLifeSpan = lifeSpan;
            mValue = obj;
            expireTimer = new Timer(delegate(object state)
            {
                //When this timer is fired, its time to delete the item.
                if (this.shouldExpire(mValue))
                    mValue = default(T);
                else
                    Touch();
            }
            );
            Touch();
        }
        public TimeSpan LifeSpan
        {
            get
            {
                return mLifeSpan;
            }
        }
        public bool IsExpired
        {
            get
            {
                if (Expires <= DateTime.Now)
                    return ShouldExpireThis(mValue);
                else
                    return false;
            }
        }
        public T Value
        {
            get { Touch(); return mValue; }
            set { mValue = value; Touch(); }
        }
        public void Touch()
        {
            mLastTouch = DateTime.Now;
            expireTimer.Change(mLifeSpan + new TimeSpan(0, 0, 10), new TimeSpan(0, 0, 0, 0, -1));
        }
        public static implicit operator T(CacheEntry<T> entry)
        {
            return entry.mValue;
        }
        public static implicit operator CacheEntry<T>(T obj)
        {
            return new CacheEntry<T>(obj);
        }
    }
}
